const Metadata = require("./lib/metadata");
const { DDM, MapDom } = require("./lib/ddm");
const {
    isFunction,
    isPropsChange,
    getPathRelateModule,
    isPropsChangeEqual,
    randomid,
    setProp,
    checkPropInFuzzy,
    hashCode
} = require("./lib/helper");
const {
    BINDERS,
    CHILDRENTAG,
    CLASSNAME,
    DATASET,
    DDMCONTAINER,
    DDMTAG,
    ELEMENTTAG,
    HANDLERS,
    IDNAME,
    PROPID,
    ISREMOVED,
    OUTERVIEW,
    PARENTVIEWTAG,
    PREREMOVED,
    SUBSCRIBE,
    USEPROPS,
    VIEWINFO,
    VIEWNAME,
    MODULEPATH,
    VIEWTAG,
    CONTEXT,
    WATCHER,
    DDMSETS,
    VIEWRENDERTYPE,
    VIEWSTREAMMAP,
    VIEWSUBJECTMAP,
    VIEWNEEDREMOVEPROPS,
    VIEWRENDERRATE,
    VIEWRENDERRATEANIMATEFRAME,
    VIEWRENDERRATEIMMEDIATELY,
    VIEWREADY
} = require("./lib/const");
const { DataSetHelper } = require("./dataset");
const EventEmitter = require('./lib/emitter');

const MACROS = {
    module({ bodyStr, props, attrs }) {
        let r = [`data-module=""`];
        Reflect.ownKeys(attrs).forEach(attr => r.push(`${attr}="${attr}"`));
        let template = `<div ${r.join(" ")}>${bodyStr}</div>`;
        return { template, data: props };
    }
};

const TAGS = {
    module: {
        template({ bodyStr, attrs }) {
            let r = [`data-module=""`];
            Reflect.ownKeys(attrs).forEach(attr => r.push(`${attr}="${attr}"`));
            return `<div ${r.join(" ")}>${bodyStr}</div>`;
        },
        selfClose: false
    }
};

const COMMONTAGS = {
    icon: {
        template({ props, option }) {
            if (props.name) {
                let t = option.tags[props.name];
                if (t) {
                    return t.template();
                } else {
                    console.warn('can not found icon of ' + props.name);
                    return '<i></i>';
                }
            } else if (props.type && props.type.info) {
                return props.type.info.template();
            } else {
                console.warn('can not found icon');
                return '<i></i>';
            }
        },
        selfClose: true
    }
}

class ViewEvent {
    constructor(target, type, data) {
        this.target = target;
        this.data = data;
        this.type = type;
        this._goon = true;
        this.currentTarget = null;
    }

    stopPropagation() {
        this._goon = false;
    }
}

class ClassNames {
    static getClassName(className, name) {
        if (className) {
            return `${className}-${name}`;
        } else {
            return name;
        }
    }

    constructor(view) {
        this._view = view;
        this._className = view.getAnnotationInfo().className || "";
    }

    add(name) {
        this._view.getElement().classList.add(ClassNames.getClassName(this._className, name));
        return this;
    }

    remove(name) {
        this._view.getElement().classList.remove(ClassNames.getClassName(this._className, name));
        return this;
    }

    has(name) {
        return this._view.getElement().classList.contains(ClassNames.getClassName(this._className, name));
    }

    toggle(name) {
        this._view.getElement().classList.toggle(ClassNames.getClassName(this._className, name));
        return this;
    }
}

class DDMSets {
    constructor() {
        this._marcos = {};
        this._directives = {};
        this._fns = {};
        this._propMacros = {};
        this._tags = {};
    }

    setMacro(name, fn) {
        this._marcos[name] = fn;
        return this;
    }

    setTagMacro(name, info) {
        this._tags[name] = info;
        return this;
    }

    setPropMacro(name, fn) {
        this._propMacros[name] = fn;
        return this;
    }

    setFunction(name, fn) {
        this._fns[name] = fn;
        return this;
    }

    setAssignDirective(name, fn) {
        this._directives[name] = fn;
        return this;
    }

    setIcon(icon) {
        let { name, info } = icon;
        this.setTagMacro(name, info);
        return this;
    }
};

class BaseView extends EventEmitter {
    constructor({ parent = null, dom = null, info = {}, name = "", useProps = [], context = null, id, _viewId }) {
        super();
        let ddmsets = new DDMSets();

        (this.icons() || []).forEach(a => ddmsets.setIcon(a));
        Object.entries(this.fns() || {}).forEach(([a, b]) => ddmsets.setFunction(a, b));
        Object.entries(this.macros() || {}).forEach(([a, b]) => ddmsets.setMacro(a, b));
        Object.entries(this.propMacros() || {}).forEach(([a, b]) => ddmsets.setPropMacro(a, b));
        Object.entries(this.directives() || {}).forEach(([a, b]) => ddmsets.setAssignDirective(a, b));

        this.oninitalize(ddmsets);
        setProp(this, VIEWREADY, false);
        setProp(this, DDMSETS, ddmsets);
        setProp(this, CONTEXT, context);
        setProp(this, PROPID, id);
        setProp(this, IDNAME, _viewId || randomid(10));
        setProp(this, PARENTVIEWTAG, parent);
        setProp(this, ELEMENTTAG, dom || context.document.body);
        setProp(this, ISREMOVED, false);
        setProp(this, PREREMOVED, false);
        setProp(this, VIEWINFO, info);
        setProp(this, VIEWRENDERTYPE, info.renderType || BaseView.RENDERTYPECONCURRENT);
        setProp(this, VIEWRENDERRATE, info.renderRate || BaseView.RENDERRATEIMMEDIATELY);
        setProp(this, VIEWNAME, name);
        setProp(this, USEPROPS, useProps);
        setProp(this, VIEWNEEDREMOVEPROPS, []);
        setProp(this, CLASSNAME, info.className || null);
        setProp(this, DDMTAG, new DDM({
            id: this.getId(),
            rate: this[VIEWRENDERRATE],
            container: this.getDDMContainer(),
            templateStr: info.template || "",
            className: info.className || "",
            binders: ({ method, parameters }) => {
                let info = this[BINDERS] || {};
                let _method = info[method];
                if (_method && this[_method]) {
                    this[_method](parameters);
                }
            },
            beforeEffect: (nodes) => {
                let ps = Promise.resolve();
                nodes.filter(a => !!a).forEach(node => {
                    if (node[VIEWTAG]) {
                        ps = ps.then(() => {
                            return node[VIEWTAG][DATASET]._commitUnload();
                        });
                    } else {
                        node.querySelectorAll && node.querySelectorAll(`[data-module="${this.getId()}"]`).forEach(node => {
                            ps = ps.then(() => {
                                return node[VIEWTAG][DATASET]._commitUnload();
                            });
                        });
                    }
                });
                return ps;
            },
            option: {
                fns: Object.assign({}, this.context.ddm.defaultFns, ddmsets._fns),
                tags: Object.assign(ddmsets._tags, this._getTags()),
                props: Object.assign({}, this.context.ddm.defaultPropMacros, ddmsets._propMacros),
                macro: Object.assign({}, this.context.ddm.defaultMacros, ddmsets._marcos, this._macros()),
                directives: Object.assign({}, this.context.ddm.defaultAssignDirectives, ddmsets._directives)
            },
            context
        }));
        this.className = new ClassNames(this);
        this.getElement().setAttribute("data-module-id", this[IDNAME]);
    }

    static RENDERTYPECONCURRENT = Symbol('concurrent');

    static RENDERTYPEONEBYONE = Symbol('onebyone');

    static RENDERRATEANIMATEFRAME = VIEWRENDERRATEANIMATEFRAME;

    static RENDERRATEIMMEDIATELY = VIEWRENDERRATEIMMEDIATELY;

    get context() {
        return this[CONTEXT];
    }

    get state() {
        return this.getCurrentState();
    }

    get request() {
        return this.context.request;
    }

    oninitalize() { }

    oncreated() { }

    onready() { }

    onupdated() { }

    oneffected() { }

    onunload() { }

    onrecover() { }

    onframesstart() { }

    onframeout() { }

    onframesend() { }

    isRemoved() {
        return this[ISREMOVED] === true;
    }

    isOuterView() {
        return this[OUTERVIEW] === true;
    }

    isRoot() {
        return this.getElement() && this.getElement().dataset.module === 'ada-root' && this.getElement().dataset.appRoot === 'root';
    }

    isAppRoot() {
        return this.getElement() && this.getElement().dataset.appRoot === this.context.name;
    }

    isRendered() {
        return this.getDDM().isRendered();
    }

    isReady() {
        return this[VIEWREADY];
    }

    getId() {
        return this[IDNAME];
    }

    getPropId() {
        return this[PROPID];
    }

    getName() {
        return this[VIEWNAME];
    }

    getPropName() {
        return this[VIEWNAME];
    }

    getModulePath() {
        if (this[VIEWINFO]) {
            let path = "";
            if (this[VIEWINFO].module) {
                path = this[VIEWINFO].module;
            } else {
                path = this.constructor[MODULEPATH];
            }
            return path;
        }
    }

    getModuleName() {
        if (this[VIEWINFO]) {
            let path = "";
            if (this[VIEWINFO].module) {
                path = this[VIEWINFO].module;
            } else {
                path = this.constructor[MODULEPATH];
            }
            if (path) {
                let a = path.split(".");
                a.pop();
                return a.join(".").replace(/\//g, ".") + "." + this.constructor.name;
            } else {
                return this.constructor.name;
            }
        }
    }

    getElement() {
        return this[ELEMENTTAG];
    }

    getParent() {
        return this[PARENTVIEWTAG];
    }

    getDDM() {
        return this[DDMTAG];
    }

    getDataSet() {
        return null;
    }

    getAnnotationInfo() {
        return this[VIEWINFO];
    }

    getDDMContainer() {
        return this[ELEMENTTAG];
    }

    getAppModulePath() {
        if (this[VIEWINFO]) {
            let path = "";
            if (this[VIEWINFO].module) {
                path = this[VIEWINFO].module;
            } else {
                path = this.constructor[MODULEPATH];
            }
            return `${this.context.name}/${path}`;
        }
    }

    getCurrentState() {
        return {};
    }

    getSnapshot() {
        return {};
    }

    getAssetURL(path) {
        if (this[VIEWINFO]) {
            let p = this.getAnnotationInfo().assets;
            if (p) {
                return (this.context.config.siteURL + p + "/" + path).replace(/[\/]+/g, "/");
            } else {
                return "";
            }
        }
    }

    getElementClassName(className = "") {
        if (!this.isRemoved()) {
            let name = this[CLASSNAME];
            if (name) {
                if (className) {
                    return `${name}-${className}`;
                } else {
                    return name;
                }
            } else {
                return className;
            }
        } else {
            return null;
        }
    }

    finder(name) {
        return !this.isRemoved() ? this.getDDM().finder(name) : [];
    }

    finders(name) {
        return !this.isRemoved() ? this.getDDM().finders(name) : [];
    }

    group(name) {
        return !this.isRemoved() ? this.getDDM().group(name) : null;
    }

    groups(name) {
        return !this.isRemoved() ? this.getDDM().groups(name) : [];
    }

    fns() {
        return {};
    }

    tags() {
        return {};
    }

    icons() {
        return [];
    }

    macros() {
        return {};
    }

    directives() {
        return {};
    }

    propMacros() {
        return {};
    }

    dispatchEvent(type, data) {
        if (!this.isRemoved() && this.isReady()) {
            let event = new ViewEvent(this, type, data), i = this;
            while (i) {
                i._triggerEvent(event);
                if (event._goon) {
                    i = i.getParent();
                } else {
                    break;
                }
            }
        }
    }

    excuteAssetScript(path) {
        if (!this.isRemoved()) {
            let src = this.getAssetURL(path);
            return new Promise((resolve) => {
                this.context.request.origin({ url: src, method: 'get' }).promise.then(({ data }) => {
                    new Function("window", data).call(this.context.window, this.context.window);
                    resolve();
                });
            });
        } else {
            return Promise.reject();
        }
    }

    excuteAssetStyle(path) {
        if (!this.isRemoved()) {
            let src = this.getAssetURL(path);
            return new Promise((resolve) => {
                if (!this.context.document.querySelector('script[src="' + src + '"]')) {
                    let _a = this.context.document.createElement("style");
                    _a.setAttribute("media", "screen");
                    _a.setAttribute("type", "text/css");
                    this.context.document.head.appendChild(_a);
                }
                resolve();
            });
        } else {
            return Promise.reject();
        }
    }

    _triggerEvent(e) {
        if (!this.isRemoved()) {
            e.currentTarget = this;
            let handlerInfo = this[HANDLERS];
            if (handlerInfo && handlerInfo[e.type]) {
                this[handlerInfo[e.type]](e);
            }
        }
    }

    _getParentUseProps() {
        return this[USEPROPS];
    }

    _getChangedProps() {
        return [];
    }

    _getUsedProps() {
        return this.getDDM().getUseProps()
    }

    _getRefreshTask(item, force) {
        let cache = item.element[VIEWTAG],
            props = item.getAttributes(),
            clazz = props["type"],
            parameter = props["parameter"],
            name = props["name"] || null,
            id = props["id"],
            useProps = props["useProps"] ? [...props["useProps"]] : [],
            app = typeof clazz === 'string';
        if (app) {
            if (clazz.split('.').pop() !== 'js') {
                clazz = `${clazz}.js`;
            }
        }
        if (!cache || (!app && cache.constructor !== clazz) ||
            (app && cache.getAppModulePath() !== clazz) || cache.isRemoved() ||
            (props.hasOwnProperty('name') && cache.getName() !== name) ||
            (props.hasOwnProperty('id') && cache.getPropId() !== id)) {
            let ps = Promise.resolve();
            if (cache && !cache.isRemoved()) {
                ps = ps.then(() => {
                    return cache._remove();
                });
            }
            if (app) {
                ps = ps.then(() => {
                    if (clazz.indexOf('./') !== -1) {
                        let path = getPathRelateModule(this, clazz);
                        return this.context.loader.getModule(path).then(clazz => {
                            return this.context.getViewInstance({
                                viewClass: clazz, parent: this,
                                dom: item.element, name, useProps,
                                context: this.context, id
                            });
                        });
                    } else {
                        let t = clazz.split('/'), appType = t.shift(), entry = t.join('/');
                        if (appType !== this.context.name) {
                            let context = this.context.manager.getContext(appType);
                            if (context) {
                                return context.boot({
                                    dom: item.element, parent: this, useProps, id,
                                    inited: false, entry
                                });
                            } else {
                                throw Error(`[ada] can not find app name of ${clazz}`);
                            }
                        } else {
                            return this.context.loader.getModule(entry).then(clazz => {
                                return this.context.getViewInstance({
                                    viewClass: clazz, parent: this,
                                    dom: item.element, name, useProps,
                                    context: this.context, id
                                });
                            });
                        }
                    }
                });
            } else {
                ps = ps.then(() => {
                    return this.context.getViewInstance({
                        viewClass: clazz, parent: this,
                        dom: item.element, name, useProps,
                        context: this.context, id
                    });
                });
            }
            return ps.then(_view => {
                if (!_view.isRemoved()) {
                    _view.context.logger.group(`CREATE CHILD[${_view.getModuleName() || ''}]`);
                    return Promise.resolve().then(() => _view.oncreated()).then(() => {
                        return _view.update(parameter).then(() => {
                            if (this.onchildadded) {
                                return this.onchildadded(_view);
                            }
                        }).then(() => _view.onready()).then(() => {
                            _view[VIEWREADY] = true;
                            _view.context.logger.groupEnd();
                            return _view.context._streamManager.subscribe(_view);
                        }).then(() => {
                            return _view;
                        });
                    });
                }
            });
        } else {
            if (cache && props.hasOwnProperty("parameter")) {
                this.context.logger.group(`PREDIFF CHILD[${cache.getModuleName() || ''}]`);
                if (force || isPropsChange(this._getChangedProps(), cache._getParentUseProps())) {
                    this.context.logger.log(!force ? `RAMIN[USED] | ${cache.getModuleName()}` : `FORCE | ${cache.getModuleName()}`, "|", this._getChangedProps());
                    return cache.update(parameter).then(() => cache.oneffected()).then(() => {
                        this.context.logger.groupEnd();
                    }).then(() => cache);
                } else {
                    this.context.logger.log(!force ? `RAMOUT[USED] | ${cache.getModuleName()}` : `FORCE | ${cache.getModuleName()}`, "|", this._getChangedProps());
                    this.context.logger.groupEnd();
                }
            }
            return Promise.resolve(undefined);
        }
    }

    _rerender() {
    }

    _refresh(force = false) {
        if (!this.isRemoved()) {
            let ps = Promise.resolve();
            if (this[VIEWRENDERTYPE] === BaseView.RENDERTYPECONCURRENT) {
                ps = ps.then(() => {
                    if (this.getDDM()) {
                        return Promise.all(this.getDDM().modules().map(item => {
                            return Promise.resolve().then(() => this._getRefreshTask(item, force)).catch(e => {
                                console.log(e);
                            });
                        }));
                    }
                });
            } else {
                this.getDDM().modules().forEach(item => {
                    ps = ps.then(() => this._getRefreshTask(item, force));
                });
            }
            return ps.then(() => {
                if (!this.isRemoved()) {
                    this[CHILDRENTAG] = this.getDDM().modules().map(item => {
                        let props = item.getAttributes(), target = item.element[VIEWTAG];
                        target[USEPROPS] = props["useProps"] ? [...props["useProps"]] : [];
                        return target;
                    }).concat(this[CHILDRENTAG].filter(child => child.isOuterView()));
                    return this.onupdated();
                }
            });
        } else {
            return Promise.resolve();
        }
    }

    _macros() {
        return {};
    }

    _getTags() {
        let _tags = this.tags(), result = {};
        Reflect.ownKeys(this.tags()).forEach(name => {
            let g = _tags[name];
            if (isFunction(g)) {
                result[name] = {
                    option: { type: g },
                    template({ bodyStr, props, attrs, generateOption }) {
                        let r = [`data-module=""`];
                        props.type = generateOption.type;
                        Reflect.ownKeys(attrs).forEach(attr => r.push(`${attr}="${attr}"`));
                        return `<div ${r.join(" ")}>${bodyStr}</div>`;
                    },
                    selfClose: false
                }
            } else {
                result[name] = g;
            }
        });
        return Object.assign(result, COMMONTAGS, this.context.ddm.tags.get(), this._tags());
    }

    _remove() {
        if (!this.isRemoved()) {
            let ps = Promise.resolve();
            let parent = this.getParent();
            if (parent && !parent.isRemoved()) {
                let _target = parent[CHILDRENTAG].indexOf(this);
                if (_target !== -1) {
                    parent[CHILDRENTAG].splice(_target, 1);
                }
                if (this[PREREMOVED] !== true && parent.onchildremoved) {
                    ps = ps.then(() => parent.onchildremoved(this));
                }
            }
            ps = ps.then(() => {
                if (this[DATASET] && this[PREREMOVED] !== true) {
                    return this[DATASET]._commitUnload();
                }
            }).then(() => {
                if (this.removeAllChild) {
                    return this.removeAllChild();
                };
            }).then(() => {
                return this.onunload();
            }).then(() => {
                if (this.getElementClassName()) {
                    this.getElement().classList.remove(this.getElementClassName());
                }
                this.getElement().innerHTML = "";
                this[VIEWNEEDREMOVEPROPS].forEach(a => {
                    this.getElement().removeAttribute(a);
                });
                this[DATASET] && this[DATASET]._remove();
                this.context._clean(this);
                this.getElement()[VIEWTAG] = null;
                Reflect.deleteProperty(this.getElement(), VIEWTAG);
                Reflect.ownKeys(this).forEach(key => {
                    this[key] = null;
                });
                this[ISREMOVED] = true;
            });
            return ps;
        }
        return Promise.resolve();
    }

    _tags() {
        return {};
    }

    _clean() {
        this[ISREMOVED] = true;
        if (this.getParent()) {
            let children = this.getParent().getChildren();
            let index = children.indexOf(this);
            if (index !== -1) {
                children.splice(index, 1);
            }
        }
        this.onunload && this.onunload();
        this.context._clean(this);
        Reflect.ownKeys(this).forEach(key => {
            if (key !== ISREMOVED) {
                this[key] = null;
            }
        });
    }
}

class View extends BaseView {
    constructor(parameters) {
        super(parameters);
        setProp(this, BINDERS, Metadata.getMetadataExtends("binder", this.constructor.prototype));
        setProp(this, SUBSCRIBE, Metadata.getMetadataExtends("subscribe", this.constructor.prototype));
        setProp(this, HANDLERS, Metadata.getMetadataExtends("handler", this.constructor.prototype));
        setProp(this, WATCHER, Metadata.getMetadataExtends("watch", this.constructor.prototype));
        setProp(this, VIEWSTREAMMAP, {});
        setProp(this, VIEWSUBJECTMAP, {});
        if (this[VIEWINFO].dataset) {
            setProp(this, DATASET, DataSetHelper.getDataSet(this[VIEWINFO].dataset, this));
        }
        if (this[VIEWINFO].stream) {
            Reflect.ownKeys(this[VIEWINFO].stream).forEach(subject => {
                let fn = this[VIEWINFO].stream[subject], streamId = hashCode(fn.toString());
                let method = this[SUBSCRIBE][subject];
                this[VIEWSTREAMMAP][streamId] = { subject, construct: fn, method };
                this[VIEWSUBJECTMAP][subject] = { streamId, method };
            });
        }
    }

    onbeforecommit() {
    }

    oncommited() {
    }

    getDataSet() {
        return this[DATASET];
    }

    getCurrentState() {
        if (this[DATASET]) {
            return this[DATASET].getRenderData();
        } else {
            return {};
        }
    }

    update(data) {
        if (!this.isRemoved()) {
            return this._updateFromParent(data);
        }
    }

    commit(type, data) {
        if (this[DATASET]) {
            return this[DATASET].commit(type, data);
        } else {
            return null;
        }
    }

    getSnapshot() {
        return {
            state: this.getCurrentState(),
            id: this.getId(),
            propId: this.getPropId(),
            name: this.getName(),
            useProps: this[USEPROPS],
            out: this.isOuterView(),
            module: this[VIEWINFO].module,
            app: this.context.config.name
        }
    }

    getStream(name) {
        let info = this[VIEWSUBJECTMAP][name];
        if (info && info.streamId) {
            let t = this.context._streamManager.get(info.streamId);
            if (t) {
                return t.stream;
            }
        }
        return null;
    }

    _updateFromParent(parameter) {
        if (this[DATASET]) {
            if (!this[DATASET].isInitalize) {
                return this[DATASET]._commitInitalize(parameter || {});
            } else if (parameter) {
                return this.commit("update", parameter);
            }
        } else {
            if (!this.isRendered()) {
                return this._render();
            }
        }
        return Promise.resolve();
    }

    _updateFromDataSet() {
        let changeProps = this._getChangedProps();
        if (this.isRendered()) {
            if (isPropsChangeEqual(changeProps, this._getUsedProps())) {
                this.context.logger.log(`RAMIN[USED] | ${this.getModuleName()}`, "|", changeProps);
                return this._render().then(() => {
                    return this._invokeWatcher(changeProps, {
                        state: 'updated'
                    });
                });
            } else {
                return this._invokeWatcher(changeProps, {
                    state: 'passed'
                }).then(() => {
                    this.context.logger.log(`RAMOUT[USED] | ${this.getModuleName()}`, "|", changeProps);
                });
            }
        } else {
            return this._render().then(() => {
                return this._invokeWatcher(changeProps, {
                    state: 'initalize'
                });
            });
        }
    }

    _updateForceFromDataSet() {
        return this._render(true);
    }

    _getChangedProps() {
        if (this[DATASET]) {
            return this[DATASET].getChangedProps();
        } else {
            return [];
        }
    }

    _invokeWatcher(changeProps, isRendered = true) {
        changeProps = changeProps.map(a => a.substring(1));
        return Reflect.ownKeys(this[WATCHER] || {}).filter(prop => {
            return prop === '*' || checkPropInFuzzy(prop, changeProps);
        }).reduce((a, prop) => {
            return a.then(() => {
                let method = this[WATCHER][prop];
                return Promise.resolve().then(() => this[method](isRendered));
            });
        }, Promise.resolve());
    }

    _rerender() {
        this.getElement().innerHTML = "";
        return Promise.resolve().then(() => {
            return this.getDDM().render(this.getCurrentState(), false);
        }).then(() => {
            if (this.context.config.develop && this.getDDM().modules().length > 0) {
                console.warn('[ada] view typeof View can not has any child [', this.getModuleName(), ']');
            }
        }).then(() => {
            return this.onupdated();
        });
    }

    _render(force = false) {
        this.context.logger.log(`> RENDER | ${this.getModuleName()} | ${force ? 'FORCE' : 'AUTO'} | ${this.getDDM().isRendered() ? 'DIFF' : 'INIT'}`);
        return this.getDDM().render(this.getCurrentState()).then(() => {
            if (this.context.config.develop && this.getDDM().modules().length > 0) {
                console.warn('[ada] view typeof View can not has any child [', this.getModuleName(), ']');
            }
            return this.onupdated();
        });
    }
}

class ViewGroup extends View {
    constructor(parameters = {}) {
        super(parameters);
        setProp(this, CHILDRENTAG, []);
    }

    onchildremoved() {
    }

    onchildadded() {
    }

    getChildAt(index = 0) {
        if (!this.isRemoved()) {
            return this[CHILDRENTAG][index];
        } else {
            return null;
        }
    }

    getChildById(id) {
        if (!this.isRemoved() && id) {
            return this.getChildren().filter(child => child.getPropId() === id);
        } else {
            return [];
        }
    }

    getChildrenByType(type) {
        if (!this.isRemoved() && type) {
            return this.getChildren().filter(child => child instanceof type);
        } else {
            return [];
        }
    }

    getChildByName(name) {
        if (!this.isRemoved() && name) {
            return this.getChildren().filter(child => child.getName() === name)[0] || null;
        } else {
            return null;
        }
    }

    getChildren() {
        if (!this.isRemoved()) {
            return this[CHILDRENTAG];
        } else {
            return [];
        }
    }

    getSnapshot() {
        return {
            state: this.getCurrentState(),
            id: this.getId(),
            propId: this.getPropId(),
            name: this.getName(),
            useProps: this[USEPROPS],
            out: this.isOuterView(),
            module: this[VIEWINFO].module,
            children: this.getChildren().map(child => {
                return child.getSnapshot();
            }),
            app: this.context.config.name
        };
    }

    tags() {
        return {};
    }

    _updateFromDataSet() {
        let changeProps = this._getChangedProps();
        if (this.isRendered()) {
            if (isPropsChange(changeProps, this._getUsedProps())) {
                this.context.logger.log(`RAMIN[USED] | ${this.getModuleName()}`, "|", changeProps);
                return this._render().then(() => {
                    return this._invokeWatcher(changeProps, {
                        state: 'updated'
                    });
                });
            } else {
                return this._invokeWatcher(changeProps, {
                    state: 'passed'
                }).then(() => {
                    this.context.logger.log(`RAMOUT[USED] | ${this.getModuleName()}`, "|", changeProps);
                });
            }
        } else {
            return this._render().then(() => {
                return this._invokeWatcher(changeProps, {
                    state: 'initalize'
                });
            });
        }
    }

    _removeChildren(children = []) {
        let ps = Promise.resolve();
        if (!this.isRemoved()) {
            if (this[VIEWRENDERTYPE] === BaseView.RENDERTYPECONCURRENT) {
                let tasks = [];
                while (children && children.length > 0) {
                    let view = children.shift();
                    if (this.removeChild) {
                        tasks.push(this.removeChild(view));
                    }
                }
                ps = ps.then(() => {
                    return Promise.all(tasks.map(a => {
                        return Promise.resolve().then(() => a).catch(e => console.log(e));
                    }));
                });
            } else {
                while (children && children.length > 0) {
                    let view = children.shift();
                    if (this.removeChild) {
                        ps = ps.then(() => this.removeChild(view));
                    }
                }
            }
        }
        return ps;
    }

    _rerender() {
        this.getElement().innerHTML = "";
        return this.getDDM().render(this.getCurrentState(), false).then(() => {
            return this._refresh(true);
        });
    }

    _render(force = false) {
        let isRender = true, ps = Promise.resolve();
        if (!force && this.isRendered()) {
            let changeProps = this._getChangedProps();
            this.context.logger.log(`> ${!isPropsChangeEqual(changeProps, this.getDDM().getPureUseProps()) ? 'RAMOUT' : 'RAMIN'}[PUREUSED] |`, changeProps);
            if (this.getDDM().getPureUseProps().length === 0 || !isPropsChangeEqual(changeProps, this.getDDM().getPureUseProps())) {
                isRender = false;
                this.context.logger.log(`> RESETSTATE | ${this.getModuleName()}`);
                this.getDDM().resetState(this.getCurrentState());
            }
        }
        if (isRender) {
            this.context.logger.log(`> RENDER | ${this.getModuleName()} | ${force ? 'FORCE' : 'AUTO'} | ${this.getDDM().isRendered() ? 'DIFF' : 'INIT'}`);
            ps = ps.then(() => {
                if (!this.isRemoved()) {
                    return this.getDDM().render(this.getCurrentState());
                }
            });
        }
        return ps.then(() => {
            return this._refresh(force);
        });
    }

    _macros() {
        return MACROS;
    }

    _tags() {
        return TAGS;
    }
}

class StaticViewGroup extends ViewGroup {
    constructor(parameters = {}) {
        super(parameters);
    }

    static addChild(type = null, { name = "", parameter = {}, container = null, attrs = {} } = {}, parent, isbond = false) {
        if (!parent.isRemoved()) {
            if (!container) {
                container = parent.context.document.body;
            } else if (container instanceof MapDom) {
                container = container.getElement();
            }
            let app = typeof type === 'string';
            let ps = Promise.resolve();
            let _dom = parent.context.document.createElement("div");
            _dom.setAttribute("data-module", `out:${parent.getId()}`);
            Reflect.ownKeys(attrs).forEach(attr => _dom.setAttribute(attr, attrs[attr]));
            if (isbond && parent.getDDMContainer().contains(container)) {
                throw Error("[ada] BondViewGroup can not append child in the DDM container element");
            }
            container.appendChild(_dom);
            if (app) {
                ps = ps.then(() => {
                    if (type.indexOf('./') !== -1) {
                        let path = getPathRelateModule(this, type);
                        return this.context.loader.getModule(path).then(clazz => {
                            return this.context.getViewInstance({
                                viewClass: clazz,
                                parent,
                                dom: _dom,
                                name,
                                useProps: [],
                                context: this.context
                            });
                        });
                    } else {
                        let t = type.split('/'), appType = t.shift(), entry = t.join('/');
                        if (appType !== parent.context.name) {
                            let context = parent.context.manager.getContext(appType);
                            if (context) {
                                return context.boot({ dom: _dom, name, parent, useProps: [], inited: false, entry });
                            } else {
                                throw Error(`[ada] can not find app name of ${type}`);
                            }
                        } else {
                            return this.context.loader.getModule(entry).then(clazz => {
                                return this.context.getViewInstance({
                                    viewClass: clazz,
                                    parent,
                                    dom: _dom,
                                    name,
                                    useProps: [],
                                    context: this.context
                                });
                            });
                        }
                    }
                });
            } else {
                ps = ps.then(() => {
                    return parent.context.getViewInstance({
                        viewClass: type, parent, name,
                        dom: _dom, useProps: [],
                        context: parent.context
                    });
                });
            }
            return ps.then(view => {
                if (!view.isRemoved()) {
                    setProp(view, OUTERVIEW, true);
                    return Promise.resolve().then(() => view.oncreated()).then(() => {
                        return view.update(parameter).then(() => {
                            parent.getChildren().push(view);
                        }).then(() => parent.onchildadded(view)).then(() => view.onready()).then(() => {
                            view[VIEWREADY] = true;
                            return view.context._streamManager.subscribe(view);
                        }).then(() => view);
                    }).then(view => {
                        return view;
                    });
                }
            });
        }
    }

    addChild(type = null, option = {}) {
        return StaticViewGroup.addChild(type, option, this, false);
    }

    removeChild(view) {
        let ps = Promise.resolve();
        if (view && !this.isRemoved()) {
            let index = this.getChildren().indexOf(view);
            if (index !== -1 && !view.isRemoved()) {
                this[CHILDRENTAG].splice(index, 1);
                ps = ps.then(() => {
                    if (!view.isRemoved() && view[DATASET] && view[PREREMOVED] !== true) {
                        return view[DATASET]._commitUnload();
                    }
                });
                ps = ps.then(() => {
                    if (!view.isRemoved()) {
                        view.getElement().parentNode.removeChild(view.getElement());
                    }
                });
            }
        }
        return ps;
    }

    removeChildAt(index = 0) {
        return this.removeChild(this.getChildren()[index]);
    }

    removeChildByName(name) {
        return this.removeChild(this.getChildByName(name));
    }

    removeChildById(id) {
        return this.removeChild(this.getChildById(id));
    }

    removeAllChild() {
        return this._removeChildren([...this.getChildren()]);
    }

    removeChildByType(type) {
        return this._removeChildren([...this.getChildrenByType(type)]);
    }

    _rerender() {
        console.warn("[ada] StaticViewGroup UnSupport _rerender()");
        return Promise.resolve();
    }

    _refresh() {
        if (this.context.config.develop && this.getDDM().modules().length > 0) {
            console.warn('[ada] view typeof StaticViewGroup can not has any child defined in DDM [', this.getModuleName(), ']');
        }
        return Promise.resolve().then(() => this.onupdated());
    }

    _macros() {
        return {};
    }

    _tags() {
        return {};
    }
}

class BondViewGroup extends ViewGroup {
    getDDMContainer() {
        if (!this[DDMCONTAINER]) {
            let ddmcontainer = this.getElement().querySelector("." + this.getElementClassName("ddmcontainer"));
            if (!ddmcontainer) {
                ddmcontainer = this.context.document.createElement("div");
                ddmcontainer.setAttribute("class", this.getElementClassName("ddmcontainer"));
                this.getElement().appendChild(ddmcontainer);
            }
            setProp(this, DDMCONTAINER, ddmcontainer);
        }
        return this[DDMCONTAINER];
    }

    addChild(type = null, option = {}) {
        return StaticViewGroup.addChild(type, option, this, true);
    }

    removeChild(view) {
        let ps = Promise.resolve();
        if (view && !this.isRemoved()) {
            let index = this.getChildren().indexOf(view);
            if (index !== -1) {
                this[CHILDRENTAG].splice(index, 1);
                if (!this.getDDMContainer().contains(view.getElement())) {
                    ps = ps.then(() => {
                        if (!view.isRemoved() && view[DATASET] && view[PREREMOVED] !== true) {
                            return view[DATASET]._commitUnload();
                        }
                    });
                    ps = ps.then(() => {
                        if (!view.isRemoved() && view.getElement().parentNode) {
                            view.getElement().parentNode.removeChild(view.getElement());
                        }
                    });
                } else {
                    throw Error("[ada] BondViewGroup can not remove child in the DDM container");
                }
            }
        }
        return ps;
    }

    removeAllChild() {
        return this._removeChildren(this.getChildren().filter(child => {
            return !this.getDDMContainer().contains(child.getElement());
        }));
    }

    removeChildByType(type) {
        return this._removeChildren(this.getChildrenByType(type).filter(child => {
            return !this.getDDMContainer().contains(child.getElement());
        }));
    }

    removeChildById(id) {
        return this._removeChildren(this.getChildById(id).filter(child => {
            return !this.getDDMContainer().contains(child.getElement());
        }));
    }

    removeChildByName(name) {
        let child = this.getChildByName(name);
        if (!this.getDDMContainer().contains(child)) {
            return this._removeChildren(child);
        }
    }

    _rerender() {
        this.getDDMContainer().innerHTML = "";
        return this.getDDM().render(this.getCurrentState(), false).then(() => {
            return this._refresh(true);
        });
    }
}

module.exports = { View, ViewGroup, StaticViewGroup, BondViewGroup };